/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.console.config.bean;

import com.google.common.collect.Lists;
import org.openislands.oi.console.config.ServiceManager;
import org.openislands.oi.console.config.annotation.CommandGroup;
import org.openislands.oi.console.config.annotation.SingleCommandGroup;
import org.openislands.oi.console.utils.ParameterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.PropertySources;
import org.springframework.core.env.PropertySourcesPropertyResolver;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

public class LoadBean implements LoadBeanHandler {
    private static final Logger log = LoggerFactory.getLogger(LoadBean.class);

    /***
     * Load support Annotation set
     */
    private static final Set<Class<? extends Annotation>> supportAnnotation = new HashSet<>(Arrays.asList(
            CommandGroup.class, Component.class, SingleCommandGroup.class
    ));

    private final List<Method> beanInjectionList = new LinkedList<>();

    @Override
    public void loadBean(Set<Class<?>> classes, Map<Class<?>, List<Object>> resultMap) {
        for (Class<?> aClass : classes) {
            end:
            {
                for (Class<? extends Annotation> annotationClass : supportAnnotation) {
                    if (AnnotationUtils.findAnnotation(aClass, annotationClass) != null) {
                        break end;
                    }
                }
                continue;
            }

            try {
                Constructor<?> constructors = aClass.getConstructors()[0];
                Class<?>[] parameterTypes = constructors.getParameterTypes();
                Object instance = constructors.newInstance(new Object[parameterTypes.length]);
                resultMap.put(aClass, Lists.newArrayList(instance));
                log.debug("load bean {}", aClass);
                // @Bean declared method
                defineBean(aClass.getDeclaredMethods());
            } catch (Exception e) {
                log.error("load class failed", e);
            }
        }
    }

    @Override
    public void initBean(Map<Class<?>, List<Object>> beanMap) {
        List<Method> next = new ArrayList<>(beanInjectionList);
        do {
            List<Method> methods = new ArrayList<>(next);
            next.clear();
            for (Method method : methods) {
                next:
                {
                    Parameter[] parameters = method.getParameters();
                    Object[] args = new Object[parameters.length];
                    for (int i = 0; i < parameters.length; i++) {
                        Value value = parameters[i].getAnnotation(Value.class);
                        if (value != null) {
                            String paramName = value.value();
                            try {
                                Object result = new PropertySourcesPropertyResolver(
                                        ((PropertySources) beanMap.get(ServiceManager.class).get(0)))
                                        .resolveRequiredPlaceholders(paramName);
                                args[i] = ParameterUtils.valueOf(parameters[i].getType(), result);
                            } catch (Exception e) {
                                log.error(e.getMessage(), e);
                            }
                        } else {
                            Class<?> paramType = parameters[i].getType();
                            args[i] = ParameterUtils.getFirst(beanMap.get(paramType));
                            if (args[i] == null) {
                                if (methods.stream().map(Method::getReturnType).anyMatch(type -> type == paramType)) {
                                    next.add(method);
                                    break next;
                                }
                            }
                        }
                    }
                    try {
                        Object object = ParameterUtils.getFirst(beanMap.get(method.getDeclaringClass()));
                        Object result = method.invoke(object, args);
                        if (result != null) {
                            List<Object> objects = beanMap.get(result.getClass());
                            if (objects != null) objects.add(result);
                            else beanMap.put(result.getClass(), Lists.newArrayList(result));
                        }
                    } catch (Exception e) {
                        log.warn(e.getMessage(), e);
                    }
                }
            }

        } while (next.size() > 0);
    }

    private void defineBean(Method[] methods) {
        for (Method method : methods) {
            if (method.isAnnotationPresent(Bean.class)) {
                beanInjectionList.add(method);
            }
        }
    }
}
